package ru.compft.common.services.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import ru.compft.common.dao.AclDao;
import ru.compft.common.services.AclSecurityService;

import javax.annotation.Resource;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * User: VVasiliev
 * Date: 02.11.12
 * Time: 17:47
 * <p/>
 * Сервис добавления и удаления прав доступа на определенные объекты, текущему пользователю.
 * Пользователь должен быть аутентифицирован и находится в SecurityContextHolder.getContext()
 * Объект должен наследоваться от класса AbstractSecureObject
 */
@Service
public class AclSecurityServiceImpl implements AclSecurityService {
    private static final Logger logger = LoggerFactory.getLogger(AclSecurityServiceImpl.class);

    @Resource(name = "aclService")
    private MutableAclService mutableAclService;

    @Resource(name = "aclDao")
    private AclDao aclDao;

    @Override
    @Transactional
    public void addPermission(String className, Long objectId, Permission permission) {
        addPermission(className, objectId, new PrincipalSid(getUsername()), permission);
    }

    @Override
    @Transactional
    public void addPermission(String className, Long objectId, Sid recipient, Permission permission) {
        final ObjectIdentity oid = new ObjectIdentityImpl(className, objectId);

        final MutableAcl acl = readAclById(oid);

        addPermission(acl, recipient, permission);

        updateAcl(acl);

        if (logger.isDebugEnabled()) {
            logger.debug("Added securedObject {" + oid + "} ACL permission {" + permission + "} for recipient " + recipient);
        }
    }

    @Override
    @Transactional
    public void deletePermission(String className, Long objectId, Sid recipient, Permission permission) {
        final ObjectIdentity oid = new ObjectIdentityImpl(className, objectId);
        final MutableAcl acl = readAclById(oid);

        // удаляем неиспользуемые Ace
        removeAces(recipient, permission, acl);

        // обновляем ACL
        updateAcl(acl);

        if (logger.isDebugEnabled()) {
            logger.debug("Deleted securedObject {" + oid + "} ACL permission {" + permission + "} for recipient " + recipient);
        }
    }

    /**
     * Обновляем права доступа.
     * В качестве параметров приходят новые права доступа, поэтому старые нужно удалить все
     *
     * @param objectId   - ID объекта, на который устанавливается право доступа
     * @param recipient  - роль или имя пользователя обладающие правом доступа к объекту
     * @param permission - новый уровень доступа(READ, WRITE, CREATE, DELETE, ADMINISTRATION) который будет заменен
     * @param className  - Класс объекта
     */
    @Override
    @Transactional
    public void updatePermission(String className, Long objectId, Sid recipient, Permission permission) {
        final ObjectIdentity oid = new ObjectIdentityImpl(className, objectId);
        // считываем все права на этот объект
        final MutableAcl acl = readAclById(oid);

        // Меняем все Ace для данного recipient
        updateAce(recipient, permission, acl);

        // обновляем ACL
        updateAcl(acl);
    }

    private void updateAce(Sid recipient, Permission permission, MutableAcl acl) {
        if (logger.isDebugEnabled()) {
            logger.debug("Try update ace. Acl={" + acl + "}, Sid={" + recipient + "}, new Permission={" + permission + "}");
        }

        final List<AccessControlEntry> entries = acl.getEntries();

        // признак того, что permission нашли и поменяли
        boolean updated = false;
        if (!CollectionUtils.isEmpty(entries)) {
            for (int i = 0; i < entries.size(); i++) {
                AccessControlEntry entry = entries.get(i);
                if (entry.getSid().equals(recipient)) {
                    acl.updateAce(i, permission);
                    updated = true;
                    // по идее не может быть больше одной записи
                    break;
                }
            }
        }

        // если не было найдено, добавляем
        if (!updated) {
            addPermission(acl, recipient, permission);
        }
    }

    /**
     * Добавляем права
     *
     * @param acl
     * @param recipient
     * @param permission
     */
    private void addPermission(MutableAcl acl, Sid recipient, Permission permission) {
        if (logger.isDebugEnabled()) {
            logger.debug("Try add permissions aces. Acl={" + acl + "}, Sid={" + recipient + "}, Permission={" + permission + "}");
        }

        final List<AccessControlEntry> entries = acl.getEntries();
        acl.insertAce(entries.size(), permission, recipient, true);
    }

    /**
     * Обновляем записи
     *
     * @param acl
     */
    private void updateAcl(MutableAcl acl) {
        if (logger.isDebugEnabled()) {
            logger.debug("Update acl aces. Acl={" + acl + "}");
        }

        mutableAclService.updateAcl(acl);
    }

    /**
     * Получаем данные для объекта
     * Если его нет, то создаем новый
     *
     * @param oid
     * @return
     */
    private MutableAcl readAclById(ObjectIdentity oid) {
        MutableAcl acl;
        try {
            acl = (MutableAcl) mutableAclService.readAclById(oid);
        } catch (NotFoundException nfe) {
            acl = mutableAclService.createAcl(oid);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Read acl by ObjectIdentity {" + oid + "}");
        }

        return acl;
    }

    /**
     * Remove all permissions associated with this particular recipient (string equality used to keep things simple)
     *
     * @param recipient
     * @param permission
     * @param acl
     */
    private void removeAces(Sid recipient, Permission permission, MutableAcl acl) {
        if (logger.isDebugEnabled()) {
            logger.debug("Try remove aces. Acl={" + acl + "}, Sid={" + recipient + "}, Permission={" + permission + "}");
        }

        final List<AccessControlEntry> entries = acl.getEntries();

        if (!CollectionUtils.isEmpty(entries)) {
            for (int i = 0; i < entries.size(); i++) {
                AccessControlEntry entry = entries.get(i);
                if (entry.getSid().equals(recipient) && entry.getPermission().equals(permission)) {
                    acl.deleteAce(i);
                }
            }
        }
    }

    private String getUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) auth.getPrincipal()).getUsername();
        } else {
            return auth.getPrincipal().toString();
        }
    }
}
